<?php
/***
 * Candy框架 数据库mysqli驱动类	
 * 
 * $Author: 刘森 (fingerboy@qq.com) $
 * $Date: 2019-08-05 10:48:32 $   
 */

declare(strict_types=1);
namespace Candy\Extend\DB\Driver;
use Candy\Extend\DB\DBServer;
use Candy\Extend\Cache\CacheServer;
use Candy\Core\Debug;

defined('CANDY') OR die('You Are A Bad Guy. o_O???');

Class Mysqli extends DBServer {
	static $count = 0;
	static $execute = 0;
	static $totalcate = 0;
	static $links = [];
	static $results = [];
	
	/**
	 * 用于获取数据库连接mysqli对象,如果已经存在mysqli对象就不在调用connect()去连接
	 */
	protected function connect(): object
	{
		//检查驱动
		if(!extension_loaded('mysqli')){
			Debug::showMessage('请开启Mysqli扩展。', 'Mysql');
		}
		
		$this->setLink();
		if(empty(self::$links['default'])){
			$mysqli = $this->getDB();
			self::$links['default'] = $mysqli;
			return $mysqli;
		} else {
			return self::$links['default'];
		}	
	}
	
	/**
	 * 获取连接对象
	 */
	protected function getDB()
	{
		self::$count++;
		Debug::addMsg('<b>第 '.self::$count.' 次连接数据库</b>', 2); 
		
		$mysqli = new \mysqli('p:'.$this->host, $this->user, $this->pass, $this->dbname);
		if ($mysqli->connect_errno > 0){
			Debug::showMessage($mysqli->connect_error, 'Mysql');
		}else{
			if (!is_null(G('CHARSET'))){
				$mysqli->query('set names '.$this->charset);
			}
			return $mysqli;
		}
	}
	
	/**
	 * 执行SQL语句的方法
	 *
	 * @param	string	$sql		用户查询的SQL语句
	 * @param	string	$method		SQL语句的类型（select,find,total,insert,update,other）
	 * @param	array	$data		为prepare方法中的?参数绑定值
	 * @return	mixed			根据不同的SQL语句返回值
	 */
	public function query(string $sql,string $method = 'other',array $data = []): array|int|string|bool
	{
		$startTime = microtime(true); 
		$this->setNull();  //初使化SQL

		$value = $this->escapeStringArray($data);
		$marr = explode('::', $method);
		$method = strtolower(array_pop($marr));
		
		if(strtolower($method) == trim('total')){
			$sql = preg_replace('/select.*?from/i','SELECT count(*) as count FROM',$sql);
		}
		
		$addcache = false;   //用于判断是否向mem中加数据
		$memkey = $this->sql($sql, $value);
		
		//随机语句不缓存
		$rand = stristr($memkey,'rand()');
		
		//转化key
		$thekey = md5($memkey);
		
		//如果之前已查询被缓存则直接返回
		if($rand === false){
			if(isset(self::$results[$this->tabName][$thekey])){
				return self::$results[$this->tabName][$thekey];
			}elseif($method == 'select' || $method == 'find' || $method=='total'){
				$addcache = true;
			}
		}
		
		//mem取缓存
		if(defined('CLIWORKING') === false){
			$memCache = '';
			$memName = 'File';
			if($rand === false){
				$memCache = CacheServer::instance();
				$memName = explode('\\', $memCache::class)[4];
				if($memName != 'File'){
					if($method == 'select' || $method == 'find' || $method=='total'){
						$data = $memCache->getCache($thekey);
						if($data){
							return $data;  //直接从memserver中取，不再向下执行
						}else{
							$addcache = true;	
						}
					}
				}
			}
		}
			
		$mysqli = $this->connect();
		
		if($mysqli)
			$stmt = $mysqli->prepare($sql);  //准备好一个语句
		else
			return;
		
		//绑定参数
		if(count($value) > 0){
			$s = str_repeat('s', count($value));
			array_unshift($value, $s);
			call_user_func_array([$stmt, 'bind_param'], $value);
		}
		if($stmt){
			$result = $stmt->execute();   //执行一个准备好的语句
		}
		
		//如果SQL有误，则输出并直接返回退出
		if(!$result){
			Debug::addMsg("<font color='red'>SQL ERROR: [{$mysqli->errno}] {$stmt->error}</font>");
			Debug::addMsg('请查看：<font color=\'#005500\'>'.$memkey.'</font>'); //debug
			return;
		}
		
		//不是查找语句
		if($addcache === false){
			self::$results[$this->tabName] = [];
		}
		
		//如果使用mem，并且不是查找语句
		if(!empty($memCache) && $addcache === false && $memName != 'File' && defined('CLIWORKING') === false){
			if($stmt->affected_rows > 0){ //有影响行数
				$memCache->delCache($this->tabName);	 //清除缓存
				Debug::addMsg("清除表<b>{$this->tabName}</b>在Memcache中所有缓存!"); //debug
			}
		}
		
		$returnv = null;
		switch($method){
			case 'select':  //查所有满足条件的
				$stmt->store_result(); 
				$data = $this->getAll($stmt);
				$returnv = $data;
				break;
			 case 'find':    //只要一条记录的
				$stmt->store_result(); 
				if($stmt->num_rows > 0){
					$data = $this->getOne($stmt);
					$returnv = $data;
				}else{
					$returnv = false;
				}
				break;
			case 'total':  //返回总记录数
				$stmt->store_result(); 
				$row = $this->getOne($stmt);
				$returnv = $data = (int)$row['count'];
				break;
			case 'insert':  //插入数据 返回最后插入的ID
				if($this->auto == 'yes')
					$returnv = $mysqli->insert_id;
				else
					$returnv = $result;
				break;
			case 'delete':
			case 'update':        //update 
				$returnv = $stmt->affected_rows;
				break;
			default:
				$returnv = $result;
		}
		self::$execute++;
		$stopTime = microtime(true);
		$ys = round(($stopTime - $startTime)*1000, 4);
		
		//添加到缓存
		if($method == 'select' || $method == 'find' || $method=='total'){
			//自己缓存
			self::$results[$this->tabName][$thekey] = $data;
			
			//不是workerman模式
			if(defined('CLIWORKING') === false){
				//mem缓存
				if($addcache && $rand === false && $memName != 'File'){
					$memCache->addCache($this->tabName, $thekey, $data);
				}
			}
		}
			
		Debug::addMsg('[用时<font color="red">'.$ys.'</font>ms] ('. self::$execute .'次) - '.$memkey,2); //debug
		self::$totalcate += $ys;
		self::$echosql = $memkey;	
		return $returnv;
	}
	
	/**
	 * 获取多所有记录
	 */
	private function getAll($stmt): array
	{
		$result = [];
		$field = $stmt->result_metadata()->fetch_fields();
		$out = [];
		//获取所有结果集中的字段名
		$fields = [];
		foreach ($field as $val){
			$fields[] = &$out[$val->name];
		}
		//用所有字段名绑定到bind_result方上
		call_user_func_array([$stmt,'bind_result'], $fields);
	       	while ($stmt->fetch()){
			$t = [];  //一条记录关联数组
			foreach ($out as $key => $val){
				$t[$key] = $val;
			}
			$result[] = $t;
		}
		return $result;  //二维数组
	}
	
	/**
	 * 获取一条记录
	 */
	private function getOne($stmt): array
	{
		$result = [];
		$field = $stmt->result_metadata()->fetch_fields();
		$out = [];
		//获取所有结果集中的字段名
		$fields = [];
		foreach ($field as $val){
			$fields[] = &$out[$val->name];
		}
		//用所有字段名绑定到bind_result方上
		call_user_func_array([$stmt,'bind_result'], $fields);
	        $stmt->fetch();
		
		foreach ($out as $key => $val){
			$result[$key] = $val;
		}
		return $result;  //一维关联数组
    }
	
	/**
	 * 自动获取表结构
	 *
	 * @param	string	$tabName	表名
	 */
	public function setTable(string $tabName): void
	{
		$this->setLink();
		$cachefile = RUNTIME . 'Data/'.$tabName.'.php';
		$this->tabName = $this->tabprefix.$tabName; //加前缀的表名
	
		if(file_exists($cachefile)){
			$json = ltrim(file_get_contents($cachefile),'<?php ');
			$this->auto = substr($json,-3);
			$json = substr($json, 0, -3);
			$this->fieldList = (array)json_decode($json, true);	
		
		}else{
			$mysqli = $this->connect();
			
			if($mysqli){
				$result = $mysqli->query("desc {$this->tabName}");
				if(!$result){
					//表不存在
					Debug::showMessage('SQLSTATE[42S02]: Base table or view not found: 1146 Table \''. $this->dbname .'.'. $this->tabName.'\' doesn\'t exist', 'Mysql');
					stop();
				}
			}
				
			$fields = [];
			$auto = 'yno';
			while($row = $result->fetch_assoc()){
				if($row['Key'] == 'PRI'){
					$fields['pri'] = strtolower($row['Field']);
				}else{
					$fields[] = strtolower($row['Field']);
				}
				if($row['Extra'] == 'auto_increment')
					$auto = 'yes';
			}
			//如果表中没有主键，则将第一列当作主键
			if(!array_key_exists('pri', $fields)){
				$fields['pri'] = array_shift($fields);		
			}
			if(isDebug() === false){
				file_put_contents($cachefile, '<?php '.json_encode($fields).$auto);
			}
			$this->fieldList = $fields;
			$this->auto = $auto;
		}
	}
	
	/**
	 * 事务开始
	 */
	public function beginTransaction(): void
	{
		$this->connect()->autocommit(false);
	}
	
	/**
 	 * 事务提交
 	 */
	public function commit(): void
	{
		$mysqli = $this->connect();
		$mysqli->commit();
    	$mysqli->autocommit(true);
	}

	/**
 	 * 事务回滚
 	 */
	public function rollBack(): void
	{
		$mysqli = $this->connect();
		$mysqli->rollback();
    	$mysqli->autocommit(true);
	}
	
	/*
	 * 获取数据库使用大小
	 *
	 * @return	string		返回转换后单位的尺寸
	 */
	public public function dbSize(): string
	{
		$sql = "SHOW TABLE STATUS FROM " . G('DBNAME');
		if(!empty($this->tabprefix)){
			$sql .= ' LIKE \''.$this->tabprefix.'%\'';
		}
		$mysqli = $this->connect();
		$result = $mysqli->query($sql);
		$size = 0;
		while($row = $result->fetch_assoc())
			$size += $row['Data_length'] + $row['Index_length'];
		return tosize($size);
	}

	/*
	 * 数据库的版本
	 *
	 * @return	string		返回数据库系统的版本
	 */
	public function dbVersion(): void
	{
		$mysqli = $this->connect();
		return $mysqli->server_info;
	}
}
